#include "Util.h"
#include "Session.h"
#include "Server.h"

#include <boost/asio.hpp>

using namespace scgi;

std::string Session::get(std::string key_) const
{
	const_iterator it1 = table.find(key_);
	if(it1 != table.end()) return it1->second;

	return "";
}

void Session::set(std::string key_, std::string value_)
{
	table[key_] = value_;
}

Session::const_iterator Session::begin() const { return table.begin(); }

Session::const_iterator Session::end() const { return table.end(); }

std::string Session::to_string()
{
	std::string buffer;
	buffer.reserve(1024);

	URLEncoder encode;

	const_iterator it1 = begin();
	while(it1 != end())
	{
		buffer.append(encode(it1->first.begin(), it1->first.end()));
		buffer.append(1, '=');
		buffer.append(encode(it1->second.begin(), it1->second.end()));
		if(++it1 != table.end()) buffer.append(1, '&');
	}

	return std::move(buffer);
}


std::string Session::get_sid_string() const
{
	return scgi::to_string(sid);
}

SessionTable::SessionTable(size_t try_count_, size_t cache_size_)
	: db(NULL)
	, stmt_create(NULL)
	, stmt_select(NULL)
	, stmt_update(NULL)
	, stmt_remove_by_expires(NULL)
	, stmt_remove_by_sid(NULL)
	, try_count(try_count_)
	, cache(cache_size_) { }

SessionTable::~SessionTable()
{
	mutex.lock();
	if(stmt_create) ::sqlite3_finalize(stmt_create);
	if(stmt_select) ::sqlite3_finalize(stmt_select);
	if(stmt_update) ::sqlite3_finalize(stmt_update);
	if(stmt_remove_by_expires) ::sqlite3_finalize(stmt_remove_by_expires);
	if(stmt_remove_by_sid) ::sqlite3_finalize(stmt_remove_by_sid);
	if(db) ::sqlite3_close(db);
	mutex.unlock();
}

size_t SessionTable::get_cache_size() { return cache.size(); }

Session::pointer SessionTable::create(Time expires_)
{
	Session::pointer session;

	mutex.lock();
	session = create_record(expires_);
	mutex.unlock();

	return session;
}

int SessionTable::remove(unsigned long long sid_)
{
	cache.get(sid_);

	mutex.lock();
	int result = remove_record_by_sid(sid_);
	mutex.unlock();

	return result;
}

Session::pointer SessionTable::get(unsigned long long sid_)
{
	Session::pointer session;

	if(!sid_) return session;

	session = cache.get(sid_);
	if(session && server_ptr->clock.now() < session->expires) return session;

	mutex.lock();
	session = select_record(sid_);
	mutex.unlock();

	return session;
}

int SessionTable::set(Session::pointer session_)
{
	mutex.lock();
	int result = update_record(session_);
	mutex.unlock();

	if(result) throw 500;

	cache.set(session_->sid, session_);

	return 0;
}

int SessionTable::open_database(std::string path_)
{
	int result = ::sqlite3_open(path_.c_str(), &db);
	return result;
}

int SessionTable::create_table()
{
	int result = ::sqlite3_exec(
		  db
		, "CREATE TABLE IF NOT EXISTS session("
			"sid INTEGER PRIMARY KEY, expires INTEGER, data TEXT);"
		, NULL
		, NULL
		, NULL);
	if(result == SQLITE_OK)
	{
		result = ::sqlite3_exec(
				db
			, "CREATE INDEX IF NOT EXISTS session_index ON session(expires);"
			, NULL
			, NULL
			, NULL);
	}

	return result;
}

int SessionTable::initialize(Server * server_ptr_, unsigned long seed_)
{
	server_ptr = server_ptr_;
	generator.seed(seed_);

	std::string create_sql(
		"INSERT INTO session(sid, expires) VALUES(?, ?);");
	int result = ::sqlite3_prepare_v2(
		db, create_sql.c_str(), create_sql.size(), &stmt_create, NULL);
	if(result == SQLITE_OK)
	{
		std::string select_sql("SELECT * FROM session WHERE sid = ?;");
		result = ::sqlite3_prepare_v2(
			db, select_sql.c_str(), select_sql.size(), &stmt_select, NULL);
	}
	if(result == SQLITE_OK)
	{
		std::string update_sql(
			"UPDATE session SET expires = ?, data = ? WHERE sid = ?;");
		result = ::sqlite3_prepare_v2(
			db, update_sql.c_str(), update_sql.size(), &stmt_update, NULL);
	}
	if(result == SQLITE_OK)
	{
		std::string sql_remove_by_expires(
			"DELETE FROM session WHERE expires < ?;");
		result = ::sqlite3_prepare_v2(
			  db
			, sql_remove_by_expires.c_str()
			, sql_remove_by_expires.size()
			, &stmt_remove_by_expires
			, NULL);
	}
	if(result == SQLITE_OK)
	{
		std::string sql_remove_by_sid(
			"DELETE FROM session WHERE sid = ?;");
		result = ::sqlite3_prepare_v2(
			  db
			, sql_remove_by_sid.c_str()
			, sql_remove_by_sid.size()
			, &stmt_remove_by_sid
			, NULL);
	}

	return result;
}

void SessionTable::tick(int tick_count_)
{
	//if(tick_count_ % (60 * 5)) return; // per five minutes.
	if(tick_count_ % 10) return;

	Time now = server_ptr->clock.now();

	mutex.lock();
	remove_record_by_expires(now);
	mutex.unlock();
}

Session::pointer SessionTable::create_record(Time expires_)
{
	std::uniform_int_distribution<unsigned long long>
		random(0, 0xFFFFFFFFFFFFFFFF);
	unsigned long long rand;
	int result;

	for(size_t i = 0; i < try_count; i++)
	{
		rand = random(generator);
		assert(rand);

		::sqlite3_reset(stmt_create);
		result = ::sqlite3_bind_int64(stmt_create, 1, rand);
		if(result != SQLITE_OK) break;

		result = ::sqlite3_bind_int64(
			stmt_create, 2, expires_.to_int_64());
		if(result != SQLITE_OK) break;

		result = ::sqlite3_step(stmt_create);
		if(result == SQLITE_OK || result == SQLITE_DONE) break;
	}
		
	Session::pointer session;

	if(result == SQLITE_OK || result == SQLITE_DONE)
	{
		session = Session::pointer(new Session());
		session->sid = rand;
		session->expires = expires_;
	}

	return session;
}

int SessionTable::remove_record_by_expires(Time time_)
{
	int result = ::sqlite3_reset(stmt_remove_by_expires);
	if(result == SQLITE_ROW || result == SQLITE_OK)
	{
		result = ::sqlite3_bind_int64(
			stmt_remove_by_expires, 1, time_.to_int_64());
	}
	if(result != SQLITE_OK) return result;

	for(size_t i = 0; i < try_count; i++)
	{
		result = ::sqlite3_step(stmt_remove_by_expires);
		if(result == SQLITE_BUSY)
		{
			assert(false); // Database file already open by another process.
			::sqlite3_sleep(1);
			continue;
		}
		break;
	}

	return (result == SQLITE_OK || result == SQLITE_DONE)
		? SQLITE_OK : result;
}

int SessionTable::remove_record_by_sid(unsigned long long sid_)
{
	int result = ::sqlite3_reset(stmt_remove_by_sid);
	if(result == SQLITE_ROW || result == SQLITE_OK)
	{
		result = ::sqlite3_bind_int64(stmt_remove_by_sid, 1, sid_);
	}
	if(result != SQLITE_OK) return result;

	for(size_t i = 0; i < try_count; i++)
	{
		result = ::sqlite3_step(stmt_remove_by_sid);
		if(result == SQLITE_BUSY)
		{
			assert(false); // Database file already open by another process.
			::sqlite3_sleep(1);
			continue;
		}
		break;
	}
		
	return (result == SQLITE_OK || result == SQLITE_DONE)
		? SQLITE_OK : result;
}

Session::pointer SessionTable::select_record(unsigned long long sid_)
{
	Session::pointer session;

	int result = ::sqlite3_reset(stmt_select);
	if(result != SQLITE_ROW && result != SQLITE_OK) return session;

	result = ::sqlite3_bind_int64(stmt_select, 1, sid_);
	if(result != SQLITE_OK) return session;

	for(size_t i = 0; i < try_count; i++)
	{
		result = ::sqlite3_step(stmt_select);
		if(result == SQLITE_BUSY)
		{
			assert(false); // Database file already open by another process.
			::sqlite3_sleep(1);
			continue;
		}
		break;
	}
		
	if(result == SQLITE_DONE || result == SQLITE_ROW)
	{
		session = Session::pointer(new Session());
		session->sid = ::sqlite3_column_int64(stmt_select, 0);
		session->expires = ::sqlite3_column_int64(stmt_select, 1);
		char const * first = reinterpret_cast<char const *>(
			::sqlite3_column_text(stmt_select, 2));
		int length = ::sqlite3_column_bytes(stmt_select, 2);
		session->assign(first, first + length);
	}

	return session;
}

int SessionTable::update_record(Session::pointer session_)
{
	int result = ::sqlite3_reset(stmt_update);
	if(result == SQLITE_ROW || result == SQLITE_OK)
	{
		result = ::sqlite3_bind_int64(
			stmt_update, 1, session_->expires.to_int_64());
	}
	if(result == SQLITE_OK)
	{
		std::string data = session_->to_string();
		result = ::sqlite3_bind_text(
			stmt_update, 2, data.c_str(), data.size(), SQLITE_TRANSIENT);
	}
	if(result == SQLITE_OK)
	{
		result = ::sqlite3_bind_int64(stmt_update, 3, session_->sid);
	}
	if(result != SQLITE_OK) return result;

	for(size_t i = 0; i < try_count; i++)
	{
		result = ::sqlite3_step(stmt_update);
		if(result == SQLITE_BUSY)
		{
			assert(false); // Database file already open by another process.
			::sqlite3_sleep(1);
			continue;
		}
		break;
	}

	return result == SQLITE_DONE ? SQLITE_OK : result;
}

